Pop Some Virtual Bubble Wrap
Pop Some Virtual Bubble Wrap

What is this?

I guess it’s bubble wrap.

Responsive image

Actually, maybe an exercise in theme swapping. A little bit of a programmatical math problem, but not a very hard one cause it could definitely be better. For sure it’s a randomized sound board depending on the selected theme. Honestly, it’s a warning about running with your main idea with no plan, then allowing for infinite scope creep. Eventually you have to find a cutoff and that’s how I ended up with what you see here.

At the very least, it’s fun to play with for a little bit, just to see what noises you get from the bubbles. I recommend “cat” if you have one.

The Wrap

The HTML Source.

I don’t always include any of the HTML in these little projects. It’s usually pretty insignificant. In this case, the theme navigation contributes pretty greatly to what theme is loaded, the amount of bubbles generated and the audio that’s played.

This little section also becomes pretty relevant during the reset button’s action. It’s pretty far down in the JS section, but you can see how I’m pulling the selected active item. It’s probably a pretty terrible way to go about things and truthfully, all the theme retention could have been handled better with cookies, but I thought of that after finishing the current set up. My main goal was to avoid setting and checking global variables, which the current setup avoids well enough.


<button type="button" 
    class="btn btn-outline-read btn-md btn-grid" 
    id="resetId" value="Reset" 
    onclick="resetPage();" 
    style="margin-bottom: 5px;">
      Reset <i class="fa-solid fa-arrows-rotate"></i>
</button>

<div>            
<span class="btn-group btn-group-toggle" data-toggle="buttons" id="button-toggle-group">
  <label class="btn btn-outline-read btn-md btn-grid active">
  <input type="radio" name="options" id="traditional" styletheme="round-button-traditional" 
            sheetsize="100" onchange="selectTheme(this.id)" autocomplete="off" checked> Traditional
  </label>
  <label class="btn btn-outline-read btn-md btn-grid active"> 
  <input type="radio" name="options" id="fun" styletheme="round-button-fun" 
            sheetsize="100" onchange="selectTheme(this.id)" autocomplete="off"> Fun
  </label>
  <label class="btn btn-outline-read btn-md btn-grid">
  <input type="radio" name="options" id="cat" styletheme="round-button-cat" 
            sheetsize="100" onchange="selectTheme(this.id)" autocomplete="off"> Cats
  </label>
  <label class="btn btn-outline-read btn-md btn-grid">
  <input type="radio" name="options" id="magnitude" styletheme="round-button-magnitude" 
            sheetsize="30" onchange="selectTheme(this.id)" autocomplete="off"> Magnitude
  </label>
  <label class="btn btn-outline-read btn-md btn-grid">
  <input type="radio" name="options" id="all" styletheme="round-button-all" 
            sheetsize="100" onchange="selectTheme(this.id)" autocomplete="off"> All 
  </label>
</span>
</div>
            
The JS Source.

Most of the JavasScript is relatively plain. The only things really worth calling out are the generateSheet function and the resetPage function.

The generateSheet function isn’t all that complicated but does a few funky checks to set certain values on the bubbles as they generate depending on the selected theme. The bubble offset is a combination of the container size and a mod check to programmatically set a margin on every other row. It’s a pretty cheap way of creating the effect, but it works. Setting the color for the “Fun” theme is just a counter in the loop where the count resets after all the colors from an array have been used in a sequence. The reset of the loop is setting things like bubble icons and styles.

The resetPage function is also pretty simple. It destroys the old sheet and sets up all the values to generate a new one. The interesting part is pulling the active tag from the class of the selected theme, then using that tag to work out the theme, audio cues, and number of bubbles to generate. It’s not complicated, but some of the attributes weren’t easily accessible. As mentioned above, all of this could have been avoided by setting globals or cookies. There’s probably other ways to get the active tag, or maybe a better way to set that flag on the elements as well.


document.onreadystatechange = function () {
  if (document.readyState == "complete") {
    generateSheet('traditional','round-button-traditional',100);
  }
};

/*
  * Handle Generating Bubble Wrap
*/
function selectTheme(audioTheme) {
  let styleTheme = document.getElementById(audioTheme).getAttribute('styletheme')
  let selectedTheme = audioTheme || 'traditional';
  let sheetSize = document.getElementById(audioTheme).getAttribute('sheetsize')

  resetPage(selectedTheme,styleTheme,sheetSize);
}

function generateSheet(audioTheme, styleTheme, sheetSize) {
  let count = 0;
  let rainbow = ['#FFADAD', '#FFD6A5', '#FDFFB6', '#CAFFBF', '#9BF6FF', '#A0C4FF', '#BDB2FF', '#FFC6FF', '#FFFFFC'];
  let wrap = document.getElementById('bubble-wrap')

  for (let i = 0; i < sheetSize; i++){
    let btn = document.createElement('button');
    btn.setAttribute('type', 'button');
    btn.setAttribute('id', i);
    btn.setAttribute('audiotheme', assignAudio(audioTheme))
    btn.setAttribute('class', styleTheme);	

    if (i%10 === 0 && parseInt(i.toString().substring(0,1))%2 && audioTheme != 'magnitude') {
      btn.style.marginLeft = '16px'
    }

    if (i%3 === 0 && (i/3)%2 === 0 && audioTheme == 'magnitude') {
      btn.style.marginLeft = '32px'
    } else if(count != 2 && audioTheme == 'magnitude') {
      count++;
    }

    if (audioTheme == 'fun') {
      btn.style.backgroundColor = rainbow[count];

      count++;

      if(count === 7){
        count = 0;
      }			
    }

    btn.addEventListener('click', popBubble);

    if (audioTheme === 'cat'){		
      let cat = document.createElement('i');
      cat.setAttribute('class', 'fa-solid fa-cat');
      btn.appendChild(cat);
    }

    if (audioTheme === 'all'){		
      let all = document.createElement('i');
      all.setAttribute('class', 'fas fa-question');
      btn.appendChild(all);
    }

    wrap.appendChild(btn)
  }
}

function popBubble() {
  let audio = new Audio(`audio/${this.getAttribute('audiotheme')}`);

  audio.play();

  theme = this.getAttribute('class');

  this.setAttribute('class', `${theme}-popped`);
  this.removeEventListener('click', popBubble);
}

function resetPage(selectedTheme,styleTheme,sheetSize) {
  if (!selectedTheme || !styleTheme || !sheetSize){

    let themeButtonToggleGroup = document.getElementById("button-toggle-group").children;

    for (let b = 0; b < themeButtonToggleGroup.length; b++){
      if (themeButtonToggleGroup[b].getAttribute('class').includes('active')){
        el = themeButtonToggleGroup[b];
        break;
      }
    }

    selectedTheme = el.childNodes[1].getAttribute('id');
    styleTheme = el.childNodes[1].getAttribute('styletheme');
    sheetSize = el.childNodes[1].getAttribute('sheetsize');
  }

  
  let childrenLen = document.getElementById("bubble-wrap").children.length

  for(let i = 0; i <= childrenLen; i++){
    let bubble = document.getElementById(i);

    if (bubble)
        bubble.parentNode.removeChild(bubble);
  }

  if (document.getElementById("bubble-wrap").children.length === 0)
    generateSheet(selectedTheme,styleTheme,sheetSize)
}

/*
  * Handle Audio
*/
function assignAudio(theme) {
  const traditional = ['bubble_crack_pop_2.mp3'];
  const fun = ['bubble_cork_1.mp3','bubble_cork_2.mp3','bubble_crack_pop_1.mp3','bubble_crack_pop_2.mp3',
               'bubble_double_drip_1.mp3','bubble_light_pop_1.mp3','bubble_light_pop_2.mp3',
               'bubble_light_pop_3.mp3','bubble_pop_1.mp3','bubble_pop_2.mp3','bubble_pop_3.mp3',
               'bubble_pop_woosh_1.mp3'];
  const cat = ['kitten_meow_1.mp3','kitten_meow_2.mp3','kitten_meow_3.mp3','long_meow_1.mp3','meow_1.mp3',
               'meow_2.mp3','meow_3.mp3','meow_4.mp3','meow_5.mp3','meow_6.mp3','meow_7.mp3',
               'meow_angry_1.mp3','meow_angry_2.mp3','meow_angry_3.mp3','perfect_meow_1.mp3',
               'purr_meow_1.mp3','purr_meow_2.mp3','purr_meow_3.mp3'];
  const magnitude = ['magnitude_pop_pop.mp3']
  const all = fun.concat(cat).concat(magnitude);
  let audioTheme;

  switch(theme){
    case 'traditional':
      audioTheme = traditional;
      break;
    case 'fun':
      audioTheme = fun;
      break;
    case 'cat':
      audioTheme = cat;
      break;
    case 'magnitude':
      audioTheme = magnitude;
      break;
    case 'all':
      audioTheme = all;
      break;
    default:
      audioTheme = traditional;
      break;
  }

  playAudio = shuffleAudio(audioTheme);

  return playAudio[0];
  }

function shuffleAudio(array) {
  let currentIndex = array.length,  randomIndex;

  // While there remain elements to shuffle.
  while (currentIndex != 0) {

    // Pick a remaining element.
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex--;

    // And swap it with the current element.
    [array[currentIndex], array[randomIndex]] = [
    array[randomIndex], array[currentIndex]];
  }

  return array;
}
          
Conclusions

Always finish your projects, even if it comes out kind of bad and you had no plan and everything about it is a little sketchy. A finished project is still better than an unfinished project.

Additional Credits

I used a bit of button effects from Creative Button Hover Collection by Yasin Softaoğlu on codepen. I believe it was specifically the first button in the "TYPE 2" section, but all of the effects were really cool.

The CSS for the round buttons originally comes from a thread on stackoverflow with the answer by user G-Cyrillus. I ended up modifying it a bit, but the original designs were from the example in the answer.